<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_28</span></h2>

  <p>rClient {</p>

  <p>static final int MAX_THREADS = 40;</p>

  <p>public static void main(String[] args)</p>

  <p>throws IOException, InterruptedException {</p>

  <p>InetAddress addr =</p>

  <p>InetAddress.getByName(null);</p>

  <p>while(true) {</p>

  <p>if(JabberClientThread.threadCount()</p>

  <p>&lt; MAX_THREADS)</p>

  <p>new JabberClientThread(addr);</p>

  <p>Thread.currentThread().sleep(100);</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>JabberClientThread构建器获取一个InetAddress，并用它打开一个套接字。大家可能已看出了这样的一个套路：Socket肯定用于创建某种Reader以及／或者Writer（或者InputStream和／或OutputStream）对象，这是运用Socket的唯一方式（当然，我们可考虑编写一、两个类，令其自动完成这些操作，避免大量重复的代码编写工作）。同样地，start()执行线程的初始化，并调用run()。在这里，消息发送给服务器，而来自服务器的信息则在屏幕上回显出来。然而，线程的“存在时间”是有限的，最终都会结束。注意在套接字创建好以后，但在构建器完成之前，假若构建器失败，套接字会被清除。否则，为套接字调用close()的责任便落到了run()方法的头上。</p>

  <p>threadcount跟踪计算目前存在的JabberClientThread对象的数量。它将作为构建器的一部分增值，并在run()退出时减值（run()退出意味着线程中止）。在MultiJabberClient.main()中，大家可以看到线程的数量会得到检查。若数量太多，则多余的暂时不创建。方法随后进入“休眠”状态。这样一来，一旦部分线程最后被中止，多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS，看看对于你使用的系统来说，建立多少线程（连接）才会使您的系统资源降低到危险程度。</p>

  <p>15.4 数据报</p>

  <p>大家迄今看到的例子使用的都是“传输控制协议”（TCP），亦称作“基于数据流的套接字”。根据该协议的设计宗旨，它具有高度的可靠性，而且能保证数据顺利抵达目的地。换言之，它允许重传那些由于各种原因半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然，这种控制与可靠性需要我们付出一些代价：TCP具有非常高的开销。</p>

  <p>还有另一种协议，名为“用户数据报协议”（UDP），它并不刻意追求数据包会完全发送出去，也不能担保它们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”（TCP当然是“可靠协议”）。听起来似乎很糟，但由于它的速度快得多，所以经常还是有用武之地的。对某些应用来说，比如声音信号的传输，如果少量数据包在半路上丢失了，那么用不着太在意，因为传输的速度显得更重要一些。大多数互联网游戏，如Diablo，采用的也是UDP协议通信，因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想一台报时服务器，如果某条消息丢失了，那么也真的不必过份紧张。另外，有些应用也许能向服务器传回一条UDP消息，以便以后能够恢复。如果在适当的时间里没有响应，消息就会丢失。</p>

  <p>Java对数据报的支持与它对TCP套接字的支持大致相同，但也存在一个明显的区别。对数据报来说，我们在客户和服务器程序都可以放置一个DatagramSocket（数据报套接字），但与ServerSocket不同，前者不会干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”，取而代之的是一个数据报陈列出来。另一项本质的区别的是对TCP套接字来说，一旦我们建好了连接，便不再需要关心谁向谁“说话”――只需通过会话流来回传送数据即可。但对数据报来说，它的数据包必须知道自己来自何处，以及打算去哪里。这意味着我们必须知道每个数据报包的这些信息，否则信息就不能正常地传递。</p>

  <p>DatagramSocket用于收发数据包，而DatagramPacket包含了具体的信息。准备接收一个数据报时，只需提供一个缓冲区，以便安置接收到的数据。数据包抵达时，通过DatagramSocket，作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的DatagramPacket构建器是：</p>

  <p>DatagramPacket(buf, buf.length)</p>

  <p>其中，buf是一个字节数组。既然buf是个数组，大家可能会奇怪为什么构建器自己不能调查出数组的长度呢？实际上我也有同感，唯一能猜到的原因就是C风格的编程使然，那里的数组不能自己告诉我们它有多大。</p>

  <p>可以重复使用数据报的接收代码，不必每次都建一个新的。每次用它的时候（再生），缓冲区内的数据都会被覆盖。</p>

  <p>缓冲区的最大容量仅受限于允许的数据报包大小，这个限制位于比64KB稍小的地方。但在许多应用程序中，我们都宁愿它变得还要小一些，特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要求。</p>

  <p>发出一个数据报时，DatagramPacket不仅需要包含正式的数据，也要包含因特网地址以及端口号，以决定它的目的地。所以用于输出DatagramPacket的构建器是：</p>

  <p>DatagramPacket(buf, length, inetAddress, port)</p>

  <p>这一次，buf（一个字节数组）已经包含了我们想发出的数据。length可以是buf的长度，但也可以更短一些，意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一个目标端口（注释②）。</p>

  <p>②：我们认为TCP和UDP端口是相互独立的。也就是说，可以在端口8080同时运行一个TCP和UDP服务程序，两者之间不会产生冲突。</p>

  <p>大家也许认为两个构建器创建了两个不同的对象：一个用于接收数据报，另一个用于发送它们。如果是好的面向对象的设计方案，会建议把它们创建成两个不同的类，而不是具有不同的行为的一个类（具体行为取决于我们如何构建对象）。这也许会成为一个严重的问题，但幸运的是，DatagramPacket的使用相当简单，我们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对TCP套接字的MultiJabberServer和MultiJabberClient例子。多个客户都会将数据报发给服务器，后者会将其反馈回最初发出消息的同样的客户。</p>

  <p>为简化从一个String里创建DatagramPacket的工作（或者从DatagramPacket里创建String），这个例子首先用到了一个工具类，名为Dgram：</p>

  <p>//: Dgram.java</p>

  <p>// A utility class to convert back and forth</p>

  <p>// Between Strings and DataGramPackets.</p>

  <p>import java.net.*;</p>

  <p>public class Dgram {</p>

  <p>public static DatagramPacket toDatagram(</p>

  <p>String s, InetAddress destIA, int destPort) {</p>

  <p>// Deprecated in Java 1.1, but it works:</p>

  <p>byte[] buf = new byte[s.length() + 1];</p>

  <p>s.getBytes(0, s.length(), buf, 0);</p>

  <p>// The correct Java 1.1 approach, but it's</p>

  <p>// Broken (it truncates the String):</p>

  <p>// byte[] buf = s.getBytes();</p>

  <p>return new DatagramPacket(buf, buf.length,</p>

  <p>destIA, destPort);</p>

  <p>}</p>

  <p>public static String toString(DatagramPacket p){</p>

  <p>// The Java 1.0 approach:</p>

  <p>// return new String(p.getData(),</p>

  <p>// 0, 0, p.getLength());</p>

  <p>// The Java 1.1 approach:</p>

  <p>return</p>

  <p>new String(p.getData(), 0, p.getLength());</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>Dgram的第一个方法采用一个String、一个InetAddress以及一个端口号作为自己的参数，将String的内容复制到一个字节缓冲区，再将缓冲区传递进入DatagramPacket构建器，从而构建一个DatagramPacket。注意缓冲区分配时的"+1"――这对防止截尾现象是非常重要的。String的getByte()方法属于一种特殊操作，能将一个字串包含的char复制进入一个字节缓冲。该方法现在已被“反对”使用；Java 1.1有一个“更好”的办法来做这个工作，但在这里却被当作注释屏蔽掉了，因为它会截掉String的部分内容。所以尽管我们在Java 1.1下编译该程序时会得到一条“反对”消息，但它的行为仍然是正确无误的（这个错误应该在你读到这里的时候修正了）。</p>

  <p>Dgram.toString()方法同时展示了Java 1.0的方法和Java 1.1的方法（两者是不同的，因为有一种新类型的String构建器）。</p>

  <p>下面是用于数据报演示的服务器代码：</p>

  <p>//: ChatterServer.java</p>

  <p>// A server that echoes datagrams</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>public class ChatterServer {</p>

  <p>static final int INPORT = 1711;</p>

  <p>private byte[] buf = new byte[1000];</p>

  <p>private DatagramPacket dp =</p>

  <p>new DatagramPacket(buf, buf.length);</p>

  <p>// Can listen &amp; send on the same socket:</p>

  <p>private DatagramSocket socket;</p>

  <p>public ChatterServer() {</p>

  <p>try {</p>

  <p>socket = new DatagramSocket(INPORT);</p>

  <p>System.out.println("Server started");</p>

  <p>while(true) {</p>

  <p>// Block until a datagram appears:</p>

  <p>socket.receive(dp);</p>

  <p>String rcvd = Dgram.toString(dp) +</p>

  <p>", from address: " + dp.getAddress() +</p>

  <p>", port: " + dp.getPort();</p>

  <p>System.out.println(rcvd);</p>

  <p>String echoString =</p>

  <p>"Echoed: " + rcvd;</p>

  <p>// Extract the address and port from the</p>

  <p>// received datagram to find out where to</p>

  <p>// send it back:</p>

  <p>DatagramPacket echo =</p>

  <p>Dgram.toDatagram(echoString,</p>

  <p>dp.getAddress(), dp.getPort());</p>

  <p>socket.send(echo);</p>

  <p>}</p>

  <p>} catch(SocketException e) {</p>

  <p>System.err.println("Can't open socket");</p>

  <p>System.exit(1);</p>

  <p>} catch(IOException e) {</p>

  <p>System.err.println("Communication error");</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>new ChatterServer();</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>ChatterServer创建了一个用来接收消息的DatagramSocket（数据报套接字），而不是在我们每次准备接收一条新消息时都新建一个。这个单一的DatagramSocket可以重复使用。它有一个端口号，因为这属于服务器，客户必须确切知道自己把数据报发到哪个地址。尽管有一个端口号，但没有为它分配因特网地址，因为它就驻留在“这”台机器内，所以知道自己的因特网地址是什么（目前是默认的localhost）。在无限while循环中，套接字被告知接收数据（receive()）。然后暂时挂起，直到一个数据报出现，再把它反馈回我们希望的接收人――DatagramPacket dp――里面。数据包（Packet）会被转换成一个字串，同时插入的还有数据包的起源因特网地址及套接字。这些信息会显示出来，然后添加一个额外的字串，指出自己已从服务器反馈回来了。</p>

  <p>大家可能会觉得有点儿迷惑。正如大家会看到的那样，许多不同的因特网地址和端口号都可能是消息的起源地――换言之，客户程序可能驻留在任何一台机器里（就这一次演示来说，它们都驻留在localhost里，但每个客户使用的端口编号是不同的）。为了将一条消息送回它真正的始发客户，需要知道那个客户的因特网地址以及端口号。幸运的是，所有这些资料均已非常周到地封装到发出消息的DatagramPacket内部，所以我们要做的全部事情就是用getAddress()和getPort()把它们取出来。利用这些资料，可以构建DatagramPacket echo――它通过与接收用的相同的套接字发送回来。除此以外，一旦套接字发出数据报，就会添加“这”台机器的因特网地址及端口信息，所以当客户接收消息时，它可以利用getAddress()和getPort()了解数据报来自何处。事实上，getAddress()和getPort()唯一不能告诉我们数据报来自何处的前提是：我们创建一个待发送的数据报，并在正式发出之前调用了getAddress()和getPort()。到数据报正式发送的时候，这台机器的地址以及端口才会写入数据报。所以我们得到了运用数据报时一项重要的原则：不必跟踪一条消息的来源地！因为它肯定保存在数据报里。事实上，对程序来说，最可靠的做法是我们不要试图跟踪，而是无论如何都从目标数据报里提取出地址以及端口信息（就象这里做的那样）。</p>

  <p>为测试服务器的运转是否正常，下面这程序将创建大量客户（线程），它们都会将数据报包发给服务器，并等候服务器把它们原样反馈回来。</p>

  <p>//: ChatterServer.java</p>

  <p>// A server that echoes datagrams</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>public class ChatterServer {</p>

  <p>static final int INPORT = 1711;</p>

  <p>private byte[] buf = new byte[1000];</p>

  <p>private DatagramPacket dp =</p>

  <p>new DatagramPacket(buf, buf.length);</p>

  <p>// Can listen &amp; send on the same socket:</p>

  <p>private DatagramSocket socket;</p>

  <p>public ChatterServer() {</p>

  <p>try {</p>

  <p>socket = new DatagramSocket(INPORT);</p>

  <p>System.out.println("Server started");</p>

  <p>while(true) {</p>

  <p>// Block until a datagram appears:</p>

  <p>socket.receive(dp);</p>

  <p>String rcvd = Dgram.toString(dp) +</p>

  <p>", from address: " + dp.getAddress() +</p>

  <p>", port: " + dp.getPort();</p>

  <p>System.out.println(rcvd);</p>

  <p>String echoString =</p>

  <p>"Echoed: " + rcvd;</p>

  <p>// Extract the address and port from the</p>

  <p>// received datagram to find out where to</p>

  <p>// send it back:</p>

  <p>DatagramPacket echo =</p>

  <p>Dgram.toDatagram(echoString,</p>

  <p>dp.getAddress(), dp.getPort());</p>

  <p>socket.send(echo);</p>

  <p>}</p>

  <p>} catch(SocketException e) {</p>

  <p>System.err.println("Can't open socket");</p>

  <p>System.exit(1);</p>

  <p>} catch(IOException e) {</p>

  <p>System.err.println("Communication error");</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>new ChatterServer();</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>ChatterClient被创建成一个线程（Thread），所以可以用多个客户来“骚扰”服务器。从中可以看到，用于接收的DatagramPacket和用于ChatterServer的那个是相似的。在构建器中，创建DatagramPacket时没有附带任何参数（自变量），因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因特网地址将成为“这台机器”（比如localhost），而且会自动分配端口编号，这从输出结果即可看出。同用于服务器的那个一样，这个DatagramPacket将同时用于发送和接收。</p>

  <p>hostAddress是我们想与之通信的那台机器的因特网地址。在程序中，如果需要创建一个准备传出去的DatagramPacket，那么必须知道一个准确的因特网地址和端口号。可以肯定的是，主机必须位于一个已知的地址和端口号上，使客户能启动与主机的“会话”。</p>

  <p>每个线程都有自己独一无二的标识号（尽管自动分配给线程的端口号是也会提供一个唯一的标识符）。在run()中，我们创建了一个String消息，其中包含了线程的标识编号以及该线程准备发送的消息编号。我们用这个字串创建一个数据报，发到主机上的指定地址；端口编号则直接从ChatterServer内的一个常数取得。一旦消息发出，receive()就会暂时被“堵塞”起来，直到服务器回复了这条消息。与消息附在一起的所有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中，尽管是一种“不可靠”协议，但仍然能够检查数据报是否到去过了它们该去的地方（这在localhost和LAN环境中是成立的，但在非本地连接中却可能出现一些错误）。</p>

  <p>运行该程序时，大家会发现每个线程都会结束。这意味着发送到服务器的每个数据报包都会回转，并反馈回正确的接收者。如果不是这样，一个或更多的线程就会挂起并进入“堵塞”状态，直到它们的输入被显露出来。</p>

  <p>大家或许认为将文件从一台机器传到另一台的唯一正确方式是通过TCP套接字，因为它们是“可靠”的。然而，由于数据报的速度非常快，所以它才是一种更好的选择。我们只需将文件分割成多个数据报，并为每个包编号。接收机器会取得这些数据包，并重新“组装”它们；一个“标题包”会告诉机器应该接收多少个包，以及组装所需的另一些重要信息。如果一个包在半路“走丢”了，接收机器会返回一个数据报，告诉发送者重传。</p>

  <p>15.5 一个Web应用</p>

  <p>现在让我们想想如何创建一个应用，令其在真实的Web环境中运行，它将把Java的优势表现得淋漓尽致。这个应用的一部分是在Web服务器上运行的一个Java程序，另一部分则是一个“程序片”或“小应用程序”（Applet），从服务器下载至浏览器（即“客户”）。这个程序片从用户那里收集信息，并将其传回Web服务器上运行的应用程序。程序的任务非常简单：程序片会询问用户的E-mail地址，并在验证这个地址合格后（没有包含空格，而且有一个@符号），将该E-mail发送给Web服务器。服务器上运行的程序则会捕获传回的数据，检查一个包含了所有E-mail地址的数据文件。如果那个地址已包含在文件里，则向浏览器反馈一条消息，说明这一情况。该消息由程序片负责显示。若是一个新地址，则将其置入列表，并通知程序片已成功添加了电子函件地址。</p>

  <p>若采用传统方式来解决这个问题，我们要创建一个包含了文本字段及一个“提交”（Submit）按钮的HTML页。用户可在文本字段里键入自己喜欢的任何内容，并毫无阻碍地提交给服务器（在客户端不进行任何检查）。提交数据的同时，Web页也会告诉服务器应对数据采取什么样的操作――知会“通用网关接口”（CGI）程序，收到这些数据后立即运行服务器。这种CGI程序通常是用Perl或C写的（有时也用C++，但要求服务器支持），而且必须能控制一切可能出现的情况。它首先会检查数据，判断是否采用了正确的格式。若答案是否定的，则CGI程序必须创建一个HTML页，对遇到的问题进行描述。这个页会转交给服务器，再由服务器反馈回用户。用户看到出错提示后，必须再试一遍提交，直到通过为止。若数据正确，CGI程序会打开数据文件，要么把电子函件地址加入文件，要么指出该地址已在数据文件里了。无论哪种情况，都必须格式化一个恰当的HTML页，以便服务器返回给用户。</p>

  <p>作为Java程序员，上述解决问题的方法显得非常笨拙。而且很自然地，我们希望一切工作都用Java完成。首先，我们会用一个Java程序片负责客户端的数据有效性校验，避免数据在服务器和客户之间传来传去，浪费时间和带宽，同时减轻服务器额外构建HTML页的负担。然后跳过Perl CGI脚本，换成在服务器上运行一个Java应用。事实上，我们在这儿已完全跳过了Web服务器，仅仅需要从程序片到服务器上运行的Java应用之间建立一个连接即可。</p>

  <p>正如大家不久就会体验到的那样，尽管看起来非常简单，但实际上有一些意想不到的问题使局面显得稍微有些复杂。用Java 1.1写程序片是最理想的，但实际上却经常行不通。到本书写作的时候，拥有Java 1.1能力的浏览器仍为数不多，而且即使这类浏览器现在非常流行，仍需考虑照顾一下那些升级缓慢的人。所以从安全的角度看，程序片代码最好只用Java 1.0编写。基于这一前提，我们不能用JAR文件来合并（压缩）程序片中的.class文件。所以，我们应尽可能减少.class文件的使用数量，以缩短下载时间。</p>

  <p>好了，再来说说我用的Web服务器（写这个示范程序时用的就是它）。它确实支持Java，但仅限于Java 1.0！所以服务器应用也必须用Java 1.0编写。</p>

  <p>15.5.1 服务器应用</p>

  <p>现在讨论一下服务器应用（程序）的问题，我把它叫作NameCollecor（名字收集器）。假如多名用户同时尝试提交他们的E-mail地址，那么会发生什么情况呢？若NameCollector使用TCP/IP套接字，那么必须运用早先介绍的多线程机制来实现对多个客户的并发控制。但所有这些线程都试图把数据写到同一个文件里，其中保存了所有E-mail地址。这便要求我们设立一种锁定机制，保证多个线程不会同时访问那个文件。一个“信号机”可在这里帮助我们达到目的，但或许还有一种更简单的方式。</p>

  <p>如果我们换用数据报，就不必使用多线程了。用单个数据报即可“侦听”进入的所有数据报。一旦监视到有进入的消息，程序就会进行适当的处理，并将答复数据作为一个数据报传回原先发出请求的那名接收者。若数据报半路上丢失了，则用户会注意到没有答复数据传回，所以可以重新提交请求。</p>

  <p>服务器应用收到一个数据报，并对它进行解读的时候，必须提取出其中的电子函件地址，并检查本机保存的数据文件，看看里面是否已经包含了那个地址（如果没有，则添加之）。所以我们现在遇到了一个新的问题。Java 1.0似乎没有足够的能力来方便地处理包含了电子函件地址的文件（Java 1.1则不然）。但是，用C轻易就可以解决这个问题。因此，我们在这儿有机会学习将一个非Java程序同Java程序连接的最简便方式。程序使用的Runtime对象包含了一个名为exec()的方法，它会独立机器上一个独立的程序，并返回一个Process（进程）对象。我们可以取得一个OutputStream，它同这个单独程序的标准输入连接在一起；并取得一个InputStream，它则同标准输出连接到一起。要做的全部事情就是用任何语言写一个程序，只要它能从标准输入中取得自己的输入数据，并将输出结果写入标准输出即可。如果有些问题不能用Java简便与快速地解决（或者想利用原有代码，不想改写），就可以考虑采用这种方法。亦可使用Java的“固有方法”（Native Method），但那要求更多的技巧，大家可以参考一下附录A。</p>

  <p>1. C程序</p>

  <p>这个非Java应用是用C写成，因为Java不适合作CGI编程；起码启动的时间不能让人满意。它的任务是管理电子函件（E-mail）地址的一个列表。标准输入会接受一个E-mail地址，程序会检查列表中的名字，判断是否存在那个地址。若不存在，就将其加入，并报告操作成功。但假如名字已在列表里了，就需要指出这一点，避免重复加入。大家不必担心自己不能完全理解下列代码的含义。它仅仅是一个演示程序，告诉你如何用其他语言写一个程序，并从Java中调用它。在这里具体采用何种语言并不重要，只要能够从标准输入中读取数据，并能写入标准输出即可。</p>

  <p>//: Listmgr.c</p>

  <p>// Used by NameCollector.java to manage</p>

  <p>// the email list file on the server</p>

  <p>#include &lt;stdio.h&gt;</p>

  <p>#include &lt;stdlib.h&gt;</p>

  <p>#include &lt;string.h&gt;</p>

  <p>#define BSIZE 250</p>

  <p>int alreadyInList(FILE* list, char* name) {</p>

  <p>char lbuf[BSIZE];</p>

  <p>// Go to the beginning of the list:</p>

  <p>fseek(list, 0, SEEK_SET);</p>

  <p>// Read each line in the list:</p>

  <p>while(fgets(lbuf, BSIZE, list)) {</p>

  <p>// Strip off the newline:</p>

  <p>char * newline = strchr(lbuf, '\n');</p>

  <p>if(newline != 0)</p>

  <p>*newline = '\0';</p>

  <p>if(strcmp(lbuf, name) == 0)</p>

  <p>return 1;</p>

  <p>}</p>

  <p>return 0;</p>

  <p>}</p>

  <p>int main() {</p>

  <p>char buf[BSIZE];</p>

  <p>FILE* list = fopen("emlist.txt", "a+t");</p>

  <p>if(list == 0) {</p>

  <p>perror("could not open emlist.txt");</p>

  <p>exit(1);</p>

  <p>}</p>

  <p>while(1) {</p>

  <p>gets(buf); /* From stdin */</p>

  <p>if(alreadyInList(list, buf)) {</p>

  <p>printf("Already in list: %s", buf);</p>

  <p>fflush(stdout);</p>

  <p>}</p>

  <p>else {</p>

  <p>fseek(list, 0, SEEK_END);</p>

  <p>fprintf(list, "%s\n", buf);</p>

  <p>fflush(list);</p>

  <p>printf("%s added to list", buf);</p>

  <p>fflush(stdout);</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>该程序假设C编译器能接受'//'样式注释（许多编译器都能，亦可换用一个C++编译器来编译这个程序）。如果你的编译器不能接受，则简单地将那些注释删掉即可。</p>

  <p>文件中的第一个函数检查我们作为第二个参数（指向一个char的指针）传递给它的名字是否已在文件中。在这儿，我们将文件作为一个FILE指针传递，它指向一个已打开的文件（文件是在main()中打开的）。函数fseek()在文件中遍历；我们在这儿用它移至文件开头。fgets()从文件list中读入一行内容，并将其置入缓冲区lbuf――不会超过规定的缓冲区长度BSIZE。所有这些工作都在一个while循环中进行，所以文件中的每一行都会读入。接下来，用strchr()找到新行字符，以便将其删掉。最后，用strcmp()比较我们传递给函数的名字与文件中的当前行。若找到一致的内容，strcmp()会返回0。函数随后会退出，并返回一个1，指出该名字已经在文件里了（注意这个函数找到相符内容后会立即返回，不会把时间浪费在检查列表剩余内容的上面）。如果找遍列表都没有发现相符的内容，则函数返回0。</p>

  <p>在main()中，我们用fopen()打开文件。第一个参数是文件名，第二个是打开文件的方式；a+表示“追加”，以及“打开”（或“创建”，假若文件尚不存在），以便到文件的末尾进行更新。fopen()函数返回的是一个FILE指针；若为0，表示打开操作失败。此时需要用perror()打印一条出错提示消息，并用exit()中止程序运行。</p>

  <p>如果文件成功打开，程序就会进入一个无限循环。调用gets(buf)的函数会从标准输入中取出一行（记住标准输入会与Java程序连接到一起），并将其置入缓冲区buf中。缓冲区的内容随后会简单地传递给alreadyInList()函数，如内容已在列表中，printf()就会将那条消息发给标准输出（Java程序正在监视它）。fflush()用于对输出缓冲区进行刷新。</p>

  <p>如果名字不在列表中，就用fseek()移到列表末尾，并用fprintf()将名字“打印”到列表末尾。随后，用printf()指出名字已成功加入列表（同样需要刷新标准输出），无限循环返回，继续等候一个新名字的进入。</p>

  <p>记住一般不能先在自己的计算机上编译此程序，再把编译好的内容上载到Web服务器，因为那台机器使用的可能是不同类的处理器和操作系统。例如，我的Web服务器安装的是Intel的CPU，但操作系统是Linux，所以必须先下载源码，再用远程命令（通过telnet）指挥Linux自带的C编译器，令其在服务器端编译好程序。</p>

  <p>2. Java程序</p>

  <p>这个程序先启动上述的C程序，再建立必要的连接，以便同它“交谈”。随后，它创建一个数据报套接字，用它“监视”或者“侦听”来自程序片的数据报包。</p>

  <p>//: NameCollector.java</p>

  <p>// Extracts email names from datagrams and stores</p>

  <p>// them inside a file, using Java 1.02.</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>public class NameCollector {</p>

  <p>final static int COLLECTOR_PORT = 8080;</p>

  <p>final static int BUFFER_SIZE = 1000;</p>

  <p>byte[] buf = new byte[BUFFER_SIZE];</p>

  <p>DatagramPacket dp =</p>

  <p>new DatagramPacket(buf, buf.length);</p>

  <p>// Can listen &amp; send on the same socket:</p>

  <p>DatagramSocket socket;</p>

  <p>Process listmgr;</p>

  <p>PrintStream nameList;</p>

  <p>DataInputStream addResult;</p>

  <p>public NameCollector() {</p>

  <p>try {</p>

  <p>listmgr =</p>

  <p>Runtime.getRuntime().exec("listmgr.exe");</p>

  <p>nameList = new PrintStream(</p>

  <p>new BufferedOutputStream(</p>

  <p>listmgr.getOutputStream()));</p>

  <p>addResult = new DataInputStream(</p>

  <p>new BufferedInputStream(</p>

  <p>listmgr.getInputStream()));</p>

  <p>} catch(IOException e) {</p>

  <p>System.err.println(</p>

  <p>"Cannot start listmgr.exe");</p>

  <p>System.exit(1);</p>

  <p>}</p>

  <p>try {</p>

  <p>socket =</p>

  <p>new DatagramSocket(COLLECTOR_PORT);</p>

  <p>System.out.println(</p>

  <p>"NameCollector Server started");</p>

  <p>while(true) {</p>

  <p>// Block until a datagram appears:</p>

  <p>socket.receive(dp);</p>

  <p>String rcvd = new String(dp.getData(),</p>

  <p>0, 0, dp.getLength());</p>

  <p>// Send to listmgr.exe standard input:</p>

  <p>nameList.println(rcvd.trim());</p>

  <p>nameList.flush();</p>

  <p>byte[] resultBuf = new byte[BUFFER_SIZE];</p>

  <p>int byteCount =</p>

  <p>addResult.read(resultBuf);</p>

  <p>if(byteCount != -1) {</p>

  <p>String result =</p>

  <p>new String(resultBuf, 0).trim();</p>

  <p>// Extract the address and port from</p>

  <p>// the received datagram to find out</p>

  <p>// where to send the reply:</p>

  <p>InetAddress senderAddress =</p>

  <p>dp.getAddress();</p>

  <p>int senderPort = dp.getPort();</p>

  <p>byte[] echoBuf = new byte[BUFFER_SIZE];</p>

  <p>result.getBytes(</p>

  <p>0, byteCount, echoBuf, 0);</p>

  <p>DatagramPacket echo =</p>

  <p>new DatagramPacket(</p>

  <p>echoBuf, echoBuf.length,</p>

  <p>senderAddress, senderPort);</p>

  <p>socket.send(echo);</p>

  <p>}</p>

  <p>else</p>

  <p>System.out.println(</p>

  <p>"Unexpected lack of result from " +</p>

  <p>"listmgr.exe");</p>

  <p>}</p>

  <p>} catch(SocketException e) {</p>

  <p>System.err.println("Can't open socket");</p>

  <p>System.exit(1);</p>

  <p>} catch(IOException e) {</p>

  <p>System.err.println("Communication error");</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>new NameCollector();</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>NameCollector中的第一个定义应该是大家所熟悉的：选定端口，创建一个数据报包，然后创建指向一个DatagramSocket的句柄。接下来的三个定义负责与C程序的连接：一个Process对象是C程序由Java程序启动之后返回的，而且那个Process对象产生了InputStream和OutputStream，分别代表C程序的标准输出和标准输入。和Java IO一样，它们理所当然地需要“封装”起来，所以我们最后得到的是一个PrintStream和DataInputStream。</p>

  <p>这个程序的所有工作都是在构建器内进行的。为启动C程序，需要取得当前的Runtime对象。我们用它调用exec()，再由后者返回Process对象。在Process对象中，大家可看到通过一简单的调用即可生成数据流：getOutputStream()和getInputStream()。从这个时候开始，我们需要考虑的全部事情就是将数据传给数据流nameList，并从addResult中取得结果。</p>

  <p>和往常一样，我们将DatagramSocket同一个端口连接到一起。在无限while循环中，程序会调用receive()――除非一个数据报到来，否则receive()会一起处于“堵塞”状态。数据报出现以后，它的内容会提取到String rcvd里。我们首先将该字串两头的空格剔除（trim），再将其发给C程序。如下所示：</p>

  <p>nameList.println(rcvd.trim());</p>

  <p>之所以能这样编码，是因为Java的exec()允许我们访问任何可执行模块，只要它能从标准输入中读，并能向标准输出中写。还有另一些方式可与非Java代码“交谈”，这将在附录A中讨论。</p>

  <p>从C程序中捕获结果就显得稍微麻烦一些。我们必须调用read()，并提供一个缓冲区，以便保存结果。read()的返回值是来自C程序的字节数。若这个值为-1，意味着某个地方出现了问题。否则，我们就将resultBuf（结果缓冲区）转换成一个字串，然后同样清除多余的空格。随后，这个字串会象往常一样进入一个DatagramPacket，并传回当初发出请求的那个同样的地址。注意发送方的地址也是我们接收到的DatagramPacket的一部分。</p>

  <p>记住尽管C程序必须在Web服务器上编译，但Java程序的编译场所可以是任意的。这是由于不管使用的是什么硬件平台和操作系统，编译得到的字节码都是一样的。就就是Java的“跨平台”兼容能力。</p>

  <p>15.5.2 NameSender程序片</p>

  <p>正如早先指出的那样，程序片必须用Java 1.0编写，使其能与绝大多数的浏览器适应。也正是由于这个原因，我们产生的类数量应尽可能地少。所以我们在这儿不考虑使用前面设计好的Dgram类，而将数据报的所有维护工作都转到代码行中进行。此外，程序片要用一个线程监视由服务器传回的响应信息，而非实现Runnable接口，用集成到程序片的一个独立线程来做这件事情。当然，这样做对代码的可读性不利，但却能产生一个单类（以及单个服务器请求）程序片：</p>

  <p>//: NameSender.java</p>

  <p>// An applet that sends an email address</p>

  <p>// as a datagram, using Java 1.02.</p>

  <p>import java.awt.*;</p>

  <p>import java.applet.*;</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>public class NameSender extends Applet</p>

  <p>implements Runnable {</p>

  <p>private Thread pl = null;</p>

  <p>private Button send = new Button(</p>

  <p>"Add email address to mailing list");</p>

  <p>private TextField t = new TextField(</p>

  <p>"type your email address here", 40);</p>

  <p>private String str = new String();</p>

  <p>private Label</p>

  <p>l = new Label(), l2 = new Label();</p>

  <p>private DatagramSocket s;</p>

  <p>private InetAddress hostAddress;</p>

  <p>private byte[] buf =</p>

  <p>new byte[NameCollector.BUFFER_SIZE];</p>

  <p>private DatagramPacket dp =</p>

  <p>new DatagramPacket(buf, buf.length);</p>

  <p>private int vcount = 0;</p>

  <p>public void init() {</p>

  <p>setLayout(new BorderLayout());</p>

  <p>Panel p = new Panel();</p>

  <p>p.setLayout(new GridLayout(2, 1));</p>

  <p>p.add(t);</p>

  <p>p.add(send);</p>

  <p>add("North", p);</p>

  <p>Panel labels = new Panel();</p>

  <p>labels.setLayout(new GridLayout(2, 1));</p>

  <p>labels.add(l);</p>

  <p>labels.add(l2);</p>

  <p>add("Center", labels);</p>

  <p>try {</p>

  <p>// Auto-assign port number:</p>

  <p>s = new DatagramSocket();</p>

  <p>hostAddress = InetAddress.getByName(</p>

  <p>getCodeBase().getHost());</p>

  <p>} catch(UnknownHostException e) {</p>

  <p>l.setText("Cannot find host");</p>

  <p>} catch(SocketException e) {</p>

  <p>l.setText("Can't open socket");</p>

  <p>}</p>

  <p>l.setText("Ready to send your email address");</p>

  <p>}</p>

  <p>public boolean action (Event evt, Object arg) {</p>

  <p>if(evt.target.equals(send)) {</p>

  <p>if(pl != null) {</p>

  <p>// pl.stop(); Deprecated in Java 1.2</p>

  <p>Thread remove = pl;</p>

  <p>pl = null;</p>

  <p>remove.interrupt();</p>

  <p>}</p>

  <p>l2.setText("");</p>

  <p>// Check for errors in email name:</p>

  <p>str = t.getText().toLowerCase().trim();</p>

  <p>if(str.indexOf(' ') != -1) {</p>

  <p>l.setText("Spaces not allowed in name");</p>

  <p>return true;</p>

  <p>}</p>

  <p>if(str.indexOf(',') != -1) {</p>

  <p>l.setText("Commas not allowed in name");</p>

  <p>return true;</p>

  <p>}</p>

  <p>if(str.indexOf('@') == -1) {</p>

  <p>l.setText("Name must include '@'");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>if(str.indexOf('@') == 0) {</p>

  <p>l.setText("Name must preceed '@'");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>String end =</p>

  <p>str.substring(str.indexOf('@'));</p>

  <p>if(end.indexOf('.') == -1) {</p>

  <p>l.setText("Portion after '@' must " +</p>

  <p>"have an extension, such as '.com'");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>// Everything's OK, so send the name. Get a</p>

  <p>// fresh buffer, so it's zeroed. For some</p>

  <p>// reason you must use a fixed size rather</p>

  <p>// than calculating the size dynamically:</p>

  <p>byte[] sbuf =</p>

  <p>new byte[NameCollector.BUFFER_SIZE];</p>

  <p>str.getBytes(0, str.length(), sbuf, 0);</p>

  <p>DatagramPacket toSend =</p>

  <p>new DatagramPacket(</p>

  <p>sbuf, 100, hostAddress,</p>

  <p>NameCollector.COLLECTOR_PORT);</p>

  <p>try {</p>

  <p>s.send(toSend);</p>

  <p>} catch(Exception e) {</p>

  <p>l.setText("Couldn't send datagram");</p>

  <p>return true;</p>

  <p>}</p>

  <p>l.setText("Sent: " + str);</p>

  <p>send.setLabel("Re-send");</p>

  <p>pl = new Thread(this);</p>

  <p>pl.start();</p>

  <p>l2.setText(</p>

  <p>"Waiting for verification " + ++vcount);</p>

  <p>}</p>

  <p>else return super.action(evt, arg);</p>

  <p>return true;</p>

  <p>}</p>

  <p>// The thread portion of the applet watches for</p>

  <p>// the reply to come back from the server:</p>

  <p>public void run() {</p>

  <p>try {</p>

  <p>s.receive(dp);</p>

  <p>} catch(Exception e) {</p>

  <p>l2.setText("Couldn't receive datagram");</p>

  <p>return;</p>

  <p>}</p>

  <p>l2.setText(new String(dp.getData(),</p>

  <p>0, 0, dp.getLength()));</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>程序片的UI（用户界面）非常简单。它包含了一个TestField（文本字段），以便我们键入一个电子函件地址；以及一个Button（按钮），用于将地址发给服务器。两个Label（标签）用于向用户报告状态信息。</p>

  <p>到现在为止，大家已能判断出DatagramSocket、InetAddress、缓冲区以及DatagramPacket都属于网络连接中比较麻烦的部分。最后，大家可看到run()方法实现了线程部分，使程序片能够“侦听”由服务器传回的响应信息。</p>

  <p>init()方法用大家熟悉的布局工具设置GUI，然后创建DatagramSocket，它将同时用于数据报的收发。</p>

  <p>action()方法只负责监视我们是否按下了“发送”（send）按钮。记住，我们已被限制在Java 1.0上面，所以不能再用较灵活的内部类了。按钮按下以后，采取的第一项行动便是检查线程pl，看看它是否为null（空）。如果不为null，表明有一个活动线程正在运行。消息首次发出时，会启动一个新线程，用它监视来自服务器的回应。所以假若有个线程正在运行，就意味着这并非用户第一次发送消息。pl句柄被设为null，同时中止原来的监视者（这是最合理的一种做法，因为stop()已被Java 1.2“反对”，这在前一章已解释过了）。</p>

  <p>无论这是否按钮被第一次按下，I2中的文字都会清除。</p>

  <p>下一组语句将检查E-mail名字是否合格。String.indexOf()方法的作用是搜索其中的非法字符。如果找到一个，就把情况报告给用户。注意进行所有这些工作时，都不必涉及网络通信，所以速度非常快，而且不会影响带宽和服务器的性能。</p>

  <p>名字校验通过以后，它会打包到一个数据报里，然后采用与前面那个数据报示例一样的方式发到主机地址和端口编号。第一个标签会发生变化，指出已成功发送出去。而且按钮上的文字也会改变，变成“重发”（resend）。这时会启动线程，第二个标签则会告诉我们程序片正在等候来自服务器的回应。</p>

  <p>线程的run()方法会利用NameSender中包含的DatagramSocket来接收数据（receive()），除非出现来自服务器的数据报包，否则receive()会暂时处于“堵塞”或者“暂停”状态。结果得到的数据包会放进NameSender的DatagramPacketdp中。数据会从包中提取出来，并置入NameSender的第二个标签。随后，线程的执行将中断，成为一个“死”线程。若某段时间里没有收到来自服务器的回应，用户可能变得不耐烦，再次按下按钮。这样做会中断当前线程（数据发出以后，会再建一个新的）。由于用一个线程来监视回应数据，所以用户在监视期间仍然可以自由使用UI。</p>

  <p>1. Web页</p>

  <p>当然，程序片必须放到一个Web页里。下面列出完整的Web页源码；稍微研究一下就可看出，我用它从自己开办的邮寄列表（Mailling List）里自动收集名字。</p>

  <p>&lt;HTML&gt;</p>

  <p>&lt;HEAD&gt;</p>

  <p>&lt;META CONTENT="text/html"&gt;</p>

  <p>&lt;TITLE&gt;</p>

  <p>Add Yourself to Bruce Eckel's Java Mailing List</p>

  <p>&lt;/TITLE&gt;</p>

  <p>&lt;/HEAD&gt;</p>

  <p>&lt;BODY LINK="#0000ff" VLINK="#800080" BGCOLOR="#ffffff"&gt;</p>

  <p>&lt;FONT SIZE=6&gt;&lt;P&gt;</p>

  <p>Add Yourself to Bruce Eckel's Java Mailing List</p>

  <p>&lt;/P&gt;&lt;/FONT&gt;</p>

  <p>The applet on this page will automatically add your email address to the mailing list, so you will receive update information about changes to the online version of "Thinking in Java," notification when the book is in print, information about upcoming Java seminars, and notification about the “Hands-on Java Seminar” Multimedia CD. Type in your email address and press the button to automatically add yourself to this mailing list. &lt;HR&gt;</p>

  <p>&lt;applet code=NameSender width=400 height=100&gt;</p>

  <p>&lt;/applet&gt;</p>

  <p>&lt;HR&gt;</p>

  <p>If after several tries, you do not get verification it means that the Java application on the server is having problems. In this case, you can add yourself to the list by sending email to</p>

  <p>&lt;A HREF="mailto:Bruce@EckelObjects.com"&gt;</p>

  <p>Bruce@EckelObjects.com&lt;/A&gt;</p>

  <p>&lt;/BODY&gt;</p>

  <p>&lt;/HTML&gt;</p>

  <p>程序片标记（&lt;applet&gt;）的使用非常简单，和第13章展示的那一个并没有什么区别。</p>

  <p>15.5.3 要注意的问题</p>

  <p>前面采取的似乎是一种完美的方法。没有CGI编程，所以在服务器启动一个CGI程序时不会出现延迟。数据报方式似乎能产生非常快的响应。此外，一旦Java 1.1得到绝大多数人的采纳，服务器端的那一部分就可完全用Java编写（尽管利用标准输入和输出同一个非Java程序连接也非常容易）。</p>

  <p>但必须注意到一些问题。其中一个特别容易忽略：由于Java应用在服务器上是连续运行的，而且会把大多数时间花在Datagram.receive()方法的等候上面，这样便为CPU带来了额外的开销。至少，我在自己的服务器上便发现了这个问题。另一方面，那个服务器上不会发生其他更多的事情。而且假如我们使用一个任务更为繁重的服务器，启动程序用“nice”（一个Unix程序，用于防止进程贪吃CPU资源）或其他等价程序即可解决问题。在许多情况下，都有必要留意象这样的一些应用――一个堵塞的receive()完全可能造成CPU的瘫痪。</p>

  <p>第二个问题涉及防火墙。可将防火墙理解成自己的本地网与因特网之间的一道墙（实际是一个专用机器或防火墙软件）。它监视进出因特网的所有通信，确保这些通信不违背预设的规则。</p>

  <p>防火墙显得多少有些保守，要求严格遵守所有规则。假如没有遵守，它们会无情地把它们拒之门外。例如，假设我们位于防火墙后面的一个网络中，开始用Web浏览器同因特网连接，防火墙要求所有传输都用可以接受的http端口同服务器连接，这个端口是80。现在来了这个Java程序片NameSender，它试图将一个数据报传到端口8080，这是为了越过“受保护”的端口范围0-1024而设置的。防火墙很自然地把它想象成最坏的情况――有人使用病毒或者非法扫描端口――根本不允许传输的继续进行。</p>

  <p>只要我们的客户建立的是与因特网的原始连接（比如通过典型的ISP接驳Internet），就不会出现此类防火墙问题。但也可能有一些重要的客户隐藏在防火墙后，他们便不能使用我们设计的程序。</p>

  <p>在学过有关Java的这么多东西以后，这是一件使人相当沮丧的事情，因为看来必须放弃在服务器上使用Java，改为学习如何编写C或Perl脚本程序。但请大家不要绝望。</p>

  <p>一个出色方案是由Sun公司提出的。如一切按计划进行，Web服务器最终都装备“小服务程序”或者“服务程序片”（Servlet）。它们负责接收来自客户的请求（经过防火墙允许的80端口）。而且不再是启动一个CGI程序，它们会启动小服务程序。根据Sun的设想，这些小服务程序都是用Java编写的，而且只能在服务器上运行。运行这种小程序的服务器会自动启动它们，令其对客户的请求进行处理。这意味着我们的所有程序都可以用Java写成（100%纯咖啡）。这显然是一种非常吸引人的想法：一旦习惯了Java，就不必换用其他语言在服务器上处理客户请求。</p>

  <p>由于只能在服务器上控制请求，所以小服务程序API没有提供GUI功能。这对NameCollector.java来说非常适合，它本来就不需要任何图形界面。</p>

  <p>在本书写作时，java.sun.com已提供了一个非常廉价的小服务程序专用服务器。Sun鼓励其他Web服务器开发者为他们的服务器软件产品加入对小服务程序的支持。</p>

  <p>15.6 Java与CGI的沟通</p>

  <p>Java程序可向一个服务器发出一个CGI请求，这与HTML表单页没什么两样。而且和HTML页一样，这个请求既可以设为GET（下载），亦可设为POST（上传）。除此以外，Java程序还可拦截CGI程序的输出，所以不必依赖程序来格式化一个新页，也不必在出错的时候强迫用户从一个页回转到另一个页。事实上，程序的外观可以做得跟以前的版本别无二致。</p>

  <p>代码也要简单一些，毕竟用CGI也不是很难就能写出来（前提是真正地理解它）。所以在这一节里，我们准备办个CGI编程速成班。为解决常规问题，将用C++创建一些CGI工具，以便我们编写一个能解决所有问题的CGI程序。这样做的好处是移植能力特别强――即将看到的例子能在支持CGI的任何系统上运行，而且不存在防火墙的问题。</p>

  <p>这个例子也阐示了如何在程序片（Applet）和CGI程序之间建立连接，以便将其方便地改编到自己的项目中。</p>

  <p>15.6.1 CGI数据的编码</p>

  <p>在这个版本中，我们将收集名字和电子函件地址，并用下述形式将其保存到文件中：</p>

  <p>First Last &lt;email@domain.com&gt;;</p>

  <p>这对任何E-mail程序来说都是一种非常方便的格式。由于只需收集两个字段，而且CGI为字段中的编码采用了一种特殊的格式，所以这里没有简便的方法。如果自己动手编制一个原始的HTML页，并加入下述代码行，即可正确地理解这一点：</p>

  <p>&lt;Form method="GET" ACTION="/cgi-bin/Listmgr2.exe"&gt;</p>

  <p>&lt;P&gt;Name: &lt;INPUT TYPE = "text" NAME = "name"</p>

  <p>VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;P&gt;Email Address: &lt;INPUT TYPE = "text"</p>

  <p>NAME = "email" VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;p&gt;&lt;input type = "submit" name = "submit" &gt; &lt;/p&gt;</p>

  <p>&lt;/Form&gt;</p>

  <p>上述代码创建了两个数据输入字段（区），名为name和email。另外还有一个submit（提交）按钮，用于收集数据，并将其发给CGI程序。Listmgr2.exe是驻留在特殊程序目录中的一个可执行文件。在我们的Web服务器上，该目录一般都叫作“cgi-bin”（注释③）。如果在那个目录里找不到该程序，结果就无法出现。填好这个表单，然后按下提交按钮，即可在浏览器的URL地址窗口里看到象下面这样的内容：</p>

  <p>http://www.myhome.com/cgi-bin/Listmgr2.exe?name=First+Last&amp;email=email@domain.com&amp;submit=Submit</p>

  <p>③：在Windows32平台下，可利用与Microsoft Office 97或其他产品配套提供的Microsoft Personal Web Server（微软个人Web服务器）进行测试。这是进行试验的最好方法，因为不必正式连入网络，可在本地环境中完成测试（速度也非常快）。如果使用的是不同的平台，或者没有Office 97或者FrontPage 98那样的产品，可到网上找一个免费的Web服务器供自己测试。</p>

  <p>当然，上述URL实际显示时是不会拆行的。从中可稍微看出如何对数据编码并传给CGI。至少有一件事情能够肯定――空格是不允许的（因为它通常用于分隔命令行参数）。所有必需的空格都用“+”号替代，每个字段都包含了字段名（具体由HTML页决定），后面跟随一个“=”号以及正式的字段数据，最后用一个“&amp;”结束。</p>

  <p>到这时，大家也许会对“+”，“=”以及“&amp;”的使用产生疑惑。假如必须在字段里使用这些字符，那么该如何声明呢？例如，我们可能使用“John &amp; MarshaSmith”这个名字，其中的“&amp;”代表“And”。事实上，它会编码成下面这个样子：</p>

  <p>John+%26+Marsha+Smith</p>

  <p>也就是说，特殊字符会转换成一个“%”，并在后面跟上它的十六进制ASCII编码。</p>

  <p>幸运的是，Java有一个工具来帮助我们进行这种编码。这是URLEncoder类的一个静态方法，名为encode()。可用下述程序来试验这个方法：</p>

  <p>//: EncodeDemo.java</p>

  <p>// Demonstration of URLEncoder.encode()</p>

  <p>import java.net.*;</p>

  <p>public class EncodeDemo {</p>

  <p>public static void main(String[] args) {</p>

  <p>String s = "";</p>

  <p>for(int i = 0; i &lt; args.length; i++)</p>

  <p>s += args[i] + " ";</p>

  <p>s = URLEncoder.encode(s.trim());</p>

  <p>System.out.println(s);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>该程序将获取一些命令行参数，把它们合并成一个由多个词构成的字串，各词之间用空格分隔（最后一个空格用String.trim()剔除了）。随后对它们进行编码，并打印出来。</p>

  <p>为调用一个CGI程序，程序片要做的全部事情就是从自己的字段或其他地方收集数据，将所有数据都编码成正确的URL样式，然后汇编到单独一个字串里。每个字段名后面都加上一个“=”符号，紧跟正式数据，再紧跟一个“&amp;”。为构建完整的CGI命令，我们将这个字串置于CGI程序的URL以及一个“?”后。这是调用所有CGI程序的标准方法。大家马上就会看到，用一个程序片能够很轻松地完成所有这些编码与合并。</p>

  <p>15.6.2 程序片</p>

  <p>程序片实际要比NameSender.java简单一些。这部分是由于很容易即可发出一个GET请求。此外，也不必等候回复信息。现在有两个字段，而非一个，但大家会发现许多程序片都是熟悉的，请比较NameSender.java。</p>

  <p>//: NameSender2.java</p>

  <p>// An applet that sends an email address</p>

  <p>// via a CGI GET, using Java 1.02.</p>

  <p>import java.awt.*;</p>

  <p>import java.applet.*;</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>public class NameSender2 extends Applet {</p>

  <p>final String CGIProgram = "Listmgr2.exe";</p>

  <p>Button send = new Button(</p>

  <p>"Add email address to mailing list");</p>

  <p>TextField name = new TextField(</p>

  <p>"type your name here", 40),</p>

  <p>email = new TextField(</p>

  <p>"type your email address here", 40);</p>

  <p>String str = new String();</p>

  <p>Label l = new Label(), l2 = new Label();</p>

  <p>int vcount = 0;</p>

  <p>public void init() {</p>

  <p>setLayout(new BorderLayout());</p>

  <p>Panel p = new Panel();</p>

  <p>p.setLayout(new GridLayout(3, 1));</p>

  <p>p.add(name);</p>

  <p>p.add(email);</p>

  <p>p.add(send);</p>

  <p>add("North", p);</p>

  <p>Panel labels = new Panel();</p>

  <p>labels.setLayout(new GridLayout(2, 1));</p>

  <p>labels.add(l);</p>

  <p>labels.add(l2);</p>

  <p>add("Center", labels);</p>

  <p>l.setText("Ready to send email address");</p>

  <p>}</p>

  <p>public boolean action (Event evt, Object arg) {</p>

  <p>if(evt.target.equals(send)) {</p>

  <p>l2.setText("");</p>

  <p>// Check for errors in data:</p>

  <p>if(name.getText().trim()</p>

  <p>.indexOf(' ') == -1) {</p>

  <p>l.setText(</p>

  <p>"Please give first and last name");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>str = email.getText().trim();</p>

  <p>if(str.indexOf(' ') != -1) {</p>

  <p>l.setText(</p>

  <p>"Spaces not allowed in email name");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>if(str.indexOf(',') != -1) {</p>

  <p>l.setText(</p>

  <p>"Commas not allowed in email name");</p>

  <p>return true;</p>

  <p>}</p>

  <p>if(str.indexOf('@') == -1) {</p>

  <p>l.setText("Email name must include '@'");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>if(str.indexOf('@') == 0) {</p>

  <div class="mbppagebreak"></div>
</body>
</html>
